package com.auroramanito.uaa.rest;

import com.auroramanito.uaa.domain.Auth;
import com.auroramanito.uaa.domain.LoginDto;
import com.auroramanito.uaa.domain.MfaType;
import com.auroramanito.uaa.domain.User;
import com.auroramanito.uaa.domain.dto.SendTotpDto;
import com.auroramanito.uaa.domain.dto.UserDTO;
import com.auroramanito.uaa.domain.dto.VerifyTotpDto;
import com.auroramanito.uaa.exception.*;
import com.auroramanito.uaa.service.EmailService;
import com.auroramanito.uaa.service.SmsService;
import com.auroramanito.uaa.service.UserCacheService;
import com.auroramanito.uaa.service.UserService;
import com.auroramanito.uaa.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.nio.file.AccessDeniedException;

@RequiredArgsConstructor
@RequestMapping("/authorize")
@RestController
public class AuthorizeResource {
    private final UserService userService;
    private final JwtUtil jwtUtil;
    private final UserCacheService userCacheService;
    private final SmsService smsService;
    private final EmailService emailService;

    @PostMapping("/register")
    public void register(@Valid @RequestBody UserDTO userDTO){

        //TODO:1 检查 username,email,mobile 都是唯一的，所以要查询数据库确保唯一。
        if(userService.isUsernameExisited(userDTO.getUsername())){
            throw new DuplicateProblem("用户名重复");
        }
        if(userService.isEmailExisted(userDTO.getEmail())){
            throw new DuplicateProblem("电子邮件重复");
        }
        if(userService.isMobileExisted(userDTO.getMobile())){
            throw new DuplicateProblem("手机号重复");
        }

        val user = User.builder()
                .username(userDTO.getUsername())
                .name(userDTO.getName())
                .email(userDTO.getEmail())
                .mobile(userDTO.getMobile())
                .password(userDTO.getPassword())
                .build();

        //TODO:2 我们需要userDto 转换成User,我们给一个默认角色（ROLE_USER），然后保存。
        userService.register(user);
    }

    /**
     * 之前：userService.login(loginDto.getUsername(),loginDto.getPassword())； 直接返回授权TOKEN
     * MFA:
     *  1、获取USER
     *  1.1 升级用户编码  (在使用SecurityConfig 配置Rest登录时,通过重写UserDetailsPasswordService的updatePassword方法。实现密码更新)
     *  1.2 验证
     *  1.3 判断usingMFA 如果是false,直接返回Token
     *  1.4 使用多因子认证。
     *  1.5 添加“X-Authenticate”请求头
     *
     * */
    @PostMapping("/token")
    public ResponseEntity<?> login(@Valid @RequestBody LoginDto loginDto) throws Exception{
        return userService.findOptionalByUsernameAndPassword(loginDto.getUsername(),loginDto.getPassword())
            .map(user ->{
                //1、升级密码编码
                userService.updatePassword(user, loginDto.getPassword());
                //2、验证
                if(!user.isEnabled()){
                    throw new UserNotEnabledProblem();
                }
                if(!user.isAccountNonExpired()){
                    throw new UserAccountExpiredProblem();
                }
                if(!user.isAccountNonLocked()){
                    throw new UserAccountLockedProblem();
                }
                if(!user.isCredentialsNonExpired()){
                    throw new UserCredentialsExpiredProblem();
                }
                //3、判断 usingMfa 如果是 false, 我们就直接返回 Token
                if (!user.isUsingMfa()) {
                    return ResponseEntity.ok().body(userService.login(loginDto.getUsername(),loginDto.getPassword()));

                }
                //4、使用多因子认证
                user.setPassword(loginDto.getPassword());
                val mfaId = userCacheService.cacheUser(user);
                //5、"X-Authenticate":"mfa"."realm=" + mfaId
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                        .header("X-Authenticate","mfa","realm=" + mfaId)
                        .build();
            })
                .orElseThrow(() -> new BadCredentialsException("用户名或密码错误"));

    }

    //选择一次登录还是二次登录
    @PutMapping("/totp")
    public void sendTotp(@Valid @RequestBody SendTotpDto sendTotpDto){
        userCacheService.retrieveUser(sendTotpDto.getMfaId())
            .flatMap(user -> userService.createTotp(user.getMfaKey()).map(totp -> Pair.of(user,totp)))
                .ifPresentOrElse(pair -> {
                    if(sendTotpDto.getMfaType() == MfaType.SMS){
                        smsService.send(pair.getFirst().getMobile(),pair.getSecond());
                    }else{
                        emailService.send(pair.getFirst().getEmail(),pair.getSecond());
                    }
                },() -> {
                    throw new InvalidTotpProblem();
                });
    }

    @PostMapping("/totp")
    public Auth verifyTotp(@Valid @RequestBody VerifyTotpDto verifyTotpDto){
        return userCacheService.verifyTotp(verifyTotpDto.getMfaId(),verifyTotpDto.getCode())
        .map(user ->  userService.login(user.getUsername(),user.getPassword())
        ).orElseThrow(InvalidTotpProblem::new);
    }

    @PostMapping("/token/refresh")
    public Auth login(@RequestHeader(name = "Authorization") String authorization,@RequestParam String refreshToken)throws AccessDeniedException{
        val PREFIX = "Bearer";
        val accessToken = authorization.replace(PREFIX,"");
        if(jwtUtil.validateRefreshToken(refreshToken)&&jwtUtil.validateAccessTokenWithoutExpiration(accessToken)){
            return new Auth(jwtUtil.createAccessTokenWithRefreshToken(refreshToken),refreshToken);
        }
        throw new AccessDeniedException("访问被拒绝");
    }
}
